package com.wanrue.tcc.repository;

import com.alibaba.fastjson.JSON;
import com.wanrue.tcc.context.Participant;
import com.wanrue.tcc.context.Transaction;
import com.wanrue.tcc.context.Xid;
import com.wanrue.tcc.enums.Result;
import com.wanrue.tcc.enums.Role;
import com.wanrue.tcc.enums.Status;
import com.wanrue.tcc.recover.RecoveryInfo;
import lombok.extern.log4j.Log4j;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.StaticScriptSource;

import java.util.*;

/**
 * redis  实现
 * Created by wenjie on 11/6/18.
 */
@Log4j
public class RedisRepository implements Repository {

    private String domain;

    private String prefix = "TCC:";

    private RedisTemplate<String, String> redisTemplate;

    public RedisRepository(RedisTemplate<String, String> redisTemplate, String domain) {
        this.domain = domain;
        this.redisTemplate = redisTemplate;
    }

    @Override
    public int create(Transaction t) {
        HashOperations<String, String, Object> hashOperations = redisTemplate.opsForHash();
        hashOperations.putAll(key(t.getXid()), transactionToMap(t));
        return 1;
    }

    @Override
    public int update(Transaction t) {
        String hkey = key(t.getXid());
        Map map = new HashMap();
        map.put("PARTNER", t.getParticipants());
        String PARTNER_STR = "";
        try {
            PARTNER_STR = JSON.toJSONString(t.getParticipants());
        } catch (Exception e) {
            e.printStackTrace();
        }
        map.put("PARTNER_STR", PARTNER_STR);
        map.put("VERSION", t.getVersion());
        HashOperations<String, String, Object> op = redisTemplate.opsForHash();
        op.putAll(hkey, map);
        return 1;
    }

    @Override
    public int update(Transaction t, Result result) {
        Long res = 0L;
        try {
            String k1 = key(t.getXid());
            String k2 = k1.split(":")[2];
            String lua = "if redis.call('HGET', KEYS[1], 'VERSION') == ARGV[1] then redis.call('HMSET', KEYS[1], 'VERSION', ARGV[2], 'STATUS', ARGV[3], 'RESULT', ARGV[4],'COUNT', ARGV[5],'UPDATE_TIME', ARGV[6]) if %s then local keys = redis.call('keys', KEYS[2]) for i, v in ipairs(keys) do redis.call('HMSET', v, 'MSTATUS',ARGV[3], 'MRESULT', ARGV[4]) end return 2 end  if %s then redis.call('DEL', KEYS[1]) end return 1 end return 0 ";
            if (result != Result.INITIAL && t.getRole() == Role.INITIATOR) {
                lua = String.format(lua, "true", "false");
            } else {
                lua = String.format(lua, "false", "false");
            }
            DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
            redisScript.setScriptSource(new StaticScriptSource(lua));
            redisScript.setResultType(Long.class);
            t.setLastUpdateTime(new Date());
            res = redisTemplate.execute(redisScript, Arrays.asList(k1, k2), t.getVersion(), t.getVersion() + 1, t.getStatus().value(), result.value(), t.getCount(), t.getLastUpdateTime());
            t.setVersion(t.getVersion() + 1);
        } catch (Exception e) {
            log.error("update log fail!", e);
        }
        return Integer.parseInt(res + "");
    }

    @Override
    public int delete() {
        Set<String> keys = redisTemplate.keys(prefix + domain + ":*");
        Set<String> key = new HashSet<>();
        for (String k : keys) {
            key.add(k.split(":")[2]);
        }
        String lua = "local keys = redis.call('keys', KEYS[1]) local ok = 0 local all = 0 for i, v in ipairs(keys) do local v1 = redis.call('HGET', v, 'RESULT') if v1 == ARGV[1] then ok = ok + 1 end all = all + 1 end if all == ok then for i, v in ipairs(keys) do redis.call('DEL',v) end end";
        DefaultRedisScript redisScript = new DefaultRedisScript();
        redisScript.setScriptSource(new StaticScriptSource(lua));
        for (String s : key) {
            redisTemplate.execute(redisScript, Arrays.asList("*" + s + "*"), Result.SUCCESS.value());
        }
        return 0;
    }

    @Override
    public Transaction findByXid(Xid xid) {
        HashOperations hashOp = redisTemplate.opsForHash();
        Transaction transaction = MapToTransaction(hashOp.entries(key(xid)));
        return transaction;
    }

    @Override
    public List<RecoveryInfo> findAllUnmodifiedSince() {
        List<RecoveryInfo> list = new ArrayList<>();
        Set<String> keys = redisTemplate.keys(prefix + domain + ":*");
        HashOperations hashOp = redisTemplate.opsForHash();
        for (String key : keys) {
            RecoveryInfo RecoveryInfo = MapToTransaction(hashOp.entries(key));
            if (RecoveryInfo.getResult() == Result.SUCCESS) continue;
            list.add(RecoveryInfo);
        }
        return list;
    }

    private String key(Xid id) {
        return prefix + domain + ":" + id.getGlobalTransactionId() + ":" + id.getBranchQualifier();
    }

    private Map transactionToMap(Transaction t) {
        Map map = new HashMap();
        map.put("GLOBAL_TX_ID", t.getXid().getGlobalTransactionId());
        map.put("BRANCH_QUALIFIER", t.getXid().getBranchQualifier());
        map.put("DOMAIN", domain);
        map.put("PARTNER", t.getParticipants());
        String PARTNER_STR = "";
        try {
            PARTNER_STR = JSON.toJSONString(t.getParticipants());
        } catch (Exception e) {
            e.printStackTrace();
        }
        map.put("PARTNER_STR", PARTNER_STR);
        map.put("STATUS", t.getStatus().value());
        map.put("TYPE", t.getRole().value());
        map.put("COUNT", t.getCount());
        map.put("VERSION", t.getVersion());
        map.put("RESULT", Result.INITIAL.value());
        map.put("CREATE_TIME", new java.sql.Timestamp(t.getCreateTime().getTime()));
        map.put("UPDATE_TIME", new java.sql.Timestamp(t.getLastUpdateTime().getTime()));
        map.put("MRESULT", Result.INITIAL.value());
        map.put("MSTATUS", Status.TRYING.value());
        return map;
    }

    private RecoveryInfo MapToTransaction(Map map) {
        RecoveryInfo t = new RecoveryInfo();
        String global_tx_id = (String) map.get("GLOBAL_TX_ID");
        String branch_qualifier = (String) map.get("BRANCH_QUALIFIER");
        Xid xid = new Xid(global_tx_id, branch_qualifier);
        t.setXid(xid);
        List<Participant> partner = (List<Participant>) map.get("PARTNER");
        t.setParticipants(partner);
        Integer status = (Integer) map.get("STATUS");
        t.setStatus(Status.valueOf(status));
        Integer type = (Integer) map.get("TYPE");
        t.setRole(Role.valueOf(type));
        Integer count = (Integer) map.get("COUNT");
        t.setCount(count);
        Integer version = (Integer) map.get("VERSION");
        t.setVersion(version);
        Integer result = (Integer) map.get("RESULT");
        t.setResult(Result.valueOf(result));
        Date create_time = (Date) map.get("CREATE_TIME");
        t.setCreateTime(create_time);
        Date update_time = (Date) map.get("UPDATE_TIME");
        t.setLastUpdateTime(update_time);
        Integer mresult = (Integer) map.get("MRESULT");
        if (mresult != null) t.setMresult(Result.valueOf(result));
        Integer mstatus = (Integer) map.get("MSTATUS");
        if (mstatus != null) t.setMstatus(Status.valueOf(mstatus));
        return t;
    }

}